// Copyright (c) 2022 by Duguang.IO Inc. All Rights Reserved.
// Author: Ethan Liu
// Date: 2022-05-05 12:43:18

package engine

import (
	"encoding/base64"
	"fmt"
	"strings"

	v1 "k8s.io/api/core/v1"
	"k8s.io/apimachinery/pkg/api/resource"
	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

// 返回命名空间
func toNamespace(name string, labels map[string]string) *v1.Namespace {
	return &v1.Namespace{
		ObjectMeta: metav1.ObjectMeta{
			Name:   name,
			Labels: labels,
		},
	}
}

func toPod(unit *Unit) *v1.Pod {
	return &v1.Pod{
		ObjectMeta: metav1.ObjectMeta{
			Name:        unit.PodSpec.Name,
			Namespace:   unit.PodSpec.Namespace,
			Annotations: unit.PodSpec.Annotations,
			Labels:      unit.PodSpec.Labels,
		},
		Spec: v1.PodSpec{
			ServiceAccountName: unit.PodSpec.ServiceAccountName,
			RestartPolicy:      v1.RestartPolicyNever,
			Volumes:            toVolumes(unit),
			Containers:         toContainers(unit),
			InitContainers:     toInitContainers(unit),
			NodeName:           unit.PodSpec.NodeName,
			NodeSelector:       unit.PodSpec.NodeSelector,
			Tolerations:        toTolerations(unit),
			ImagePullSecrets:   toImagePullSecrets(unit),
			HostAliases:        toHostAliases(unit),
			DNSConfig:          toDnsConfig(unit),
		},
	}
}

func toContainers(unit *Unit) []v1.Container {
	var containers []v1.Container
	for _, s := range unit.Runners {
		containers = append(containers, toContainer(s, unit))
	}
	return containers
}

func toInitContainers(unit *Unit) []v1.Container {
	var containers []v1.Container
	// for _, s := range unit.Internal {
	// 	c := toContainer(s, unit)
	// 	c.Image = s.Image
	// 	containers = append(containers, c)
	// }
	return containers
}

func toContainer(runner *Runner, unit *Unit) v1.Container {
	return v1.Container{
		Name:            runner.ID,
		Image:           runner.Placeholder,
		Command:         runner.Entrypoint,
		Args:            runner.Command,
		ImagePullPolicy: toPullPolicy(runner.Pull),
		WorkingDir:      runner.WorkingDir,
		Resources:       toResources(runner.Resources),
		SecurityContext: toSecurityContext(runner),
		VolumeMounts:    toVolumeMounts(unit, runner),
		Env:             toEnv(unit, runner),
	}
}

// 创建环境变量列表
func toEnv(unit *Unit, runner *Runner) []v1.EnvVar {
	var envVars []v1.EnvVar

	for k := range runner.Envs {
		envVars = append(envVars, v1.EnvVar{
			Name: k,
			ValueFrom: &v1.EnvVarSource{
				ConfigMapKeyRef: &v1.ConfigMapKeySelector{
					LocalObjectReference: v1.LocalObjectReference{
						Name: unit.PodSpec.Name,
					},
					Key:      unit.PodSpec.Name + "." + k,
					Optional: boolptr(true),
				},
			},
		})
	}

	for _, secret := range runner.Secrets {
		envVars = append(envVars, v1.EnvVar{
			Name: secret.Env,
			ValueFrom: &v1.EnvVarSource{
				SecretKeyRef: &v1.SecretKeySelector{
					LocalObjectReference: v1.LocalObjectReference{
						Name: unit.PodSpec.Name,
					},
					Key:      secret.Name,
					Optional: boolptr(true),
				},
			},
		})
	}

	envVars = append(envVars, v1.EnvVar{
		Name: "KUBERNETES_NODE",
		ValueFrom: &v1.EnvVarSource{
			FieldRef: &v1.ObjectFieldSelector{
				FieldPath: "spec.nodeName",
			},
		},
	})

	return envVars
}

func toPullPolicy(policy PullPolicy) v1.PullPolicy {
	switch policy {
	case PullAlways:
		return v1.PullAlways
	case PullNever:
		return v1.PullNever
	case PullIfNotExists:
		return v1.PullIfNotPresent
	default:
		return v1.PullIfNotPresent
	}
}

func toResources(src Resources) v1.ResourceRequirements {
	var dst v1.ResourceRequirements
	if src.Limits.Memory > 0 || src.Limits.CPU > 0 {
		dst.Limits = v1.ResourceList{}
		if src.Limits.Memory > int64(0) {
			dst.Limits[v1.ResourceMemory] = *resource.NewQuantity(
				src.Limits.Memory, resource.BinarySI)
		}
		if src.Limits.CPU > int64(0) {
			dst.Limits[v1.ResourceCPU] = *resource.NewMilliQuantity(
				src.Limits.CPU, resource.DecimalSI)
		}
	}
	if src.Requests.Memory > 0 || src.Requests.CPU > 0 {
		dst.Requests = v1.ResourceList{}
		if src.Requests.Memory > int64(0) {
			dst.Requests[v1.ResourceMemory] = *resource.NewQuantity(
				src.Requests.Memory, resource.BinarySI)
		}
		if src.Requests.CPU > int64(0) {
			dst.Requests[v1.ResourceCPU] = *resource.NewMilliQuantity(
				src.Requests.CPU, resource.DecimalSI)
		}
	}
	return dst
}

func toSecurityContext(runner *Runner) *v1.SecurityContext {
	return &v1.SecurityContext{
		Privileged: boolptr(runner.Privileged),
		RunAsUser:  runner.User,
		RunAsGroup: runner.Group,
	}
}

func toVolumeMounts(unit *Unit, runner *Runner) []v1.VolumeMount {
	var volumeMounts []v1.VolumeMount
	for _, v := range runner.Volumes {
		id, ok := lookupVolumeID(unit, v.Name)
		if !ok {
			continue
		}
		volumeMounts = append(volumeMounts, v1.VolumeMount{
			Name:      id,
			MountPath: v.Path,
			SubPath:   v.SubPath,
			ReadOnly:  v.ReadOnly,
		})
	}

	return volumeMounts
}

// LookupVolume is a helper function that will lookup
// the id for a volume.
func lookupVolumeID(unit *Unit, name string) (string, bool) {
	for _, v := range unit.Volumes {
		if v.EmptyDir != nil && v.EmptyDir.Name == name {
			return v.EmptyDir.ID, true
		}

		if v.HostPath != nil && v.HostPath.Name == name {
			return v.HostPath.ID, true
		}

		if v.Claim != nil && v.Claim.Name == name {
			return v.Claim.ID, true
		}

		if v.ConfigMap != nil && v.ConfigMap.Name == name {
			return v.ConfigMap.ID, true
		}

		if v.Secret != nil && v.Secret.Name == name {
			return v.Secret.ID, true
		}

		if v.DownwardAPI != nil && v.DownwardAPI.Name == name {
			return v.DownwardAPI.ID, true
		}
	}

	return "", false
}

func toTolerations(unit *Unit) []v1.Toleration {
	var tolerations []v1.Toleration
	for _, toleration := range unit.PodSpec.Tolerations {
		t := v1.Toleration{
			Key:      toleration.Key,
			Operator: v1.TolerationOperator(toleration.Operator),
			Effect:   v1.TaintEffect(toleration.Effect),
			Value:    toleration.Value,
		}
		if toleration.TolerationSeconds != nil {
			t.TolerationSeconds = int64ptr(int64(*toleration.TolerationSeconds))
		}
		tolerations = append(tolerations, t)
	}
	return tolerations
}

func int64ptr(v int64) *int64 {
	return &v
}

func toImagePullSecrets(unit *Unit) []v1.LocalObjectReference {
	var pullSecrets []v1.LocalObjectReference
	if unit.PullSecret != nil {
		pullSecrets = []v1.LocalObjectReference{{
			Name: unit.PullSecret.Name,
		}}
	}
	return pullSecrets
}

func toHostAliases(unit *Unit) []v1.HostAlias {
	var hostAliases []v1.HostAlias
	for _, hostAlias := range unit.PodSpec.HostAliases {
		if len(hostAlias.Hostnames) > 0 {
			hostAliases = append(hostAliases, v1.HostAlias{
				IP:        hostAlias.IP,
				Hostnames: hostAlias.Hostnames,
			})
		}
	}
	return hostAliases
}

func toDnsConfig(unit *Unit) *v1.PodDNSConfig {
	var dnsOptions []v1.PodDNSConfigOption
	if len(unit.PodSpec.DnsConfig.Options) > 0 {
		for _, option := range unit.PodSpec.DnsConfig.Options {
			o := v1.PodDNSConfigOption{
				Name:  option.Name,
				Value: option.Value,
			}
			dnsOptions = append(dnsOptions, o)
		}
	}
	return &v1.PodDNSConfig{
		Nameservers: unit.PodSpec.DnsConfig.Nameservers,
		Searches:    unit.PodSpec.DnsConfig.Searches,
		Options:     dnsOptions,
	}
}

func toVolumes(unit *Unit) []v1.Volume {
	var volumes []v1.Volume
	for _, v := range unit.Volumes {
		if v.EmptyDir != nil {
			source := &v1.EmptyDirVolumeSource{}
			if strings.EqualFold(v.EmptyDir.Medium, "memory") {
				source.Medium = v1.StorageMediumMemory
				if v.EmptyDir.SizeLimit > int64(0) {
					source.SizeLimit = resource.NewQuantity(v.EmptyDir.SizeLimit, resource.BinarySI)
				}
			}
			volume := v1.Volume{
				Name: v.EmptyDir.ID,
				VolumeSource: v1.VolumeSource{
					EmptyDir: source,
				},
			}
			volumes = append(volumes, volume)
		}

		if v.HostPath != nil {
			var hostPathType v1.HostPathType
			if v.HostPath.Type == "file" {
				hostPathType = v1.HostPathFileOrCreate
			} else {
				hostPathType = v1.HostPathDirectoryOrCreate
			}
			volume := v1.Volume{
				Name: v.HostPath.ID,
				VolumeSource: v1.VolumeSource{
					HostPath: &v1.HostPathVolumeSource{
						Path: v.HostPath.Path,
						Type: &hostPathType,
					},
				},
			}
			volumes = append(volumes, volume)
		}

		if v.Claim != nil {
			volume := v1.Volume{
				Name: v.Claim.ID,
				VolumeSource: v1.VolumeSource{
					PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{
						ClaimName: v.Claim.ClaimName,
						ReadOnly:  v.Claim.ReadOnly,
					},
				},
			}
			volumes = append(volumes, volume)
		}

		if v.ConfigMap != nil {
			volume := v1.Volume{
				Name: v.ConfigMap.ID,
				VolumeSource: v1.VolumeSource{
					ConfigMap: &v1.ConfigMapVolumeSource{
						LocalObjectReference: v1.LocalObjectReference{
							Name: v.ConfigMap.ConfigMapName,
						},
						Optional:    &v.ConfigMap.Optional,
						DefaultMode: &v.ConfigMap.DefaultMode,
					},
				},
			}
			volumes = append(volumes, volume)
		}

		if v.Secret != nil {
			volume := v1.Volume{
				Name: v.Secret.ID,
				VolumeSource: v1.VolumeSource{
					Secret: &v1.SecretVolumeSource{
						SecretName:  v.Secret.SecretName,
						Optional:    &v.Secret.Optional,
						DefaultMode: &v.Secret.DefaultMode,
					},
				},
			}
			volumes = append(volumes, volume)
		}

		if v.DownwardAPI != nil {
			var items []v1.DownwardAPIVolumeFile

			for _, item := range v.DownwardAPI.Items {
				items = append(items, v1.DownwardAPIVolumeFile{
					Path: item.Path,
					FieldRef: &v1.ObjectFieldSelector{
						FieldPath: item.FieldPath,
					},
				})
			}

			volume := v1.Volume{
				Name: v.DownwardAPI.ID,
				VolumeSource: v1.VolumeSource{
					DownwardAPI: &v1.DownwardAPIVolumeSource{
						Items: items,
					},
				},
			}

			volumes = append(volumes, volume)
		}
	}

	return volumes
}

func toDockerConfigSecret(unit *Unit) *v1.Secret {
	return &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: unit.PullSecret.Name,
		},
		Type: "kubernetes.io/dockerconfigjson",
		StringData: map[string]string{
			".dockerconfigjson": unit.PullSecret.Data,
		},
	}
}

func toSecret(unit *Unit) *v1.Secret {
	stringData := make(map[string]string)
	for _, secret := range unit.Secrets {
		sDec, err := base64.StdEncoding.DecodeString(secret.Data)
		if err != nil {
			fmt.Println(err)
			continue
		}
		stringData[secret.Name] = string(sDec)
	}

	return &v1.Secret{
		ObjectMeta: metav1.ObjectMeta{
			Name: unit.PodSpec.Name,
		},
		Type:       "Opaque",
		StringData: stringData,
	}
}

func toConfigMap(unit *Unit) *v1.ConfigMap {
	stringData := make(map[string]string)
	for _, runner := range unit.Runners {
		for k, v := range runner.Envs {
			stringData[unit.PodSpec.Name+"."+k] = v
		}
	}

	return &v1.ConfigMap{
		ObjectMeta: metav1.ObjectMeta{
			Name: unit.PodSpec.Name,
		},
		Data: stringData,
	}
}

func boolptr(v bool) *bool {
	return &v
}
